// © 2016 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html
/*
 *   Copyright (C) 1996-2016, International Business Machines
 *   Corporation and others.  All Rights Reserved.
 */

package com.ibm.icu.util;

import com.ibm.icu.impl.Grego;
import java.io.IOException;
import java.util.Date;

/**
 * {@icuenhanced java.util.SimpleTimeZone}.{@icu _usage_}
 *
 * <p><code>SimpleTimeZone</code> is a concrete subclass of <code>TimeZone</code> that represents a
 * time zone for use with a Gregorian calendar. This class does not handle historical changes.
 *
 * <p>Use a negative value for <code>dayOfWeekInMonth</code> to indicate that <code>SimpleTimeZone
 * </code> should count from the end of the month backwards. For example, if Daylight Savings Time
 * starts or ends at the last Sunday in a month, use <code>dayOfWeekInMonth = -1</code> along with
 * <code>dayOfWeek = Calendar.SUNDAY</code> to specify the rule.
 *
 * @see Calendar
 * @see GregorianCalendar
 * @see TimeZone
 * @author Deborah Goldsmith, Mark Davis, Chen-Lieh Huang, Alan Liu
 * @stable ICU 2.0
 */
public class SimpleTimeZone extends BasicTimeZone implements Cloneable {
    private static final long serialVersionUID = -7034676239311322769L;

    /**
     * Constant for a mode of start or end time specified as local wall time.
     *
     * @stable ICU 3.8
     */
    public static final int WALL_TIME = 0;

    /**
     * Constant for a mode of start or end time specified as local standard time.
     *
     * @stable ICU 3.8
     */
    public static final int STANDARD_TIME = 1;

    /**
     * Constant for a mode of start or end time specified as UTC.
     *
     * @stable ICU 3.8
     */
    public static final int UTC_TIME = 2;

    /**
     * Constructs a SimpleTimeZone with the given base time zone offset from GMT and time zone ID.
     * Timezone IDs can be obtained from TimeZone.getAvailableIDs. Normally you should use
     * TimeZone.getDefault to construct a TimeZone.
     *
     * @param rawOffset The given base time zone offset to GMT.
     * @param ID The time zone ID which is obtained from TimeZone.getAvailableIDs.
     * @stable ICU 2.0
     */
    public SimpleTimeZone(int rawOffset, String ID) {
        super(ID);
        construct(rawOffset, 0, 0, 0, 0, WALL_TIME, 0, 0, 0, 0, WALL_TIME, Grego.MILLIS_PER_HOUR);
    }

    /**
     * Constructs a SimpleTimeZone with the given base time zone offset from GMT, time zone ID, time
     * to start and end the daylight time. Timezone IDs can be obtained from
     * TimeZone.getAvailableIDs. Normally you should use TimeZone.getDefault to create a TimeZone.
     * For a time zone that does not use daylight saving time, do not use this constructor; instead
     * you should use SimpleTimeZone(rawOffset, ID).
     *
     * <p>By default, this constructor specifies day-of-week-in-month rules. That is, if the
     * startDay is 1, and the startDayOfWeek is SUNDAY, then this indicates the first Sunday in the
     * startMonth. A startDay of -1 likewise indicates the last Sunday. However, by using negative
     * or zero values for certain parameters, other types of rules can be specified.
     *
     * <p>Day of month. To specify an exact day of the month, such as March 1, set startDayOfWeek to
     * zero.
     *
     * <p>Day of week after day of month. To specify the first day of the week occurring on or after
     * an exact day of the month, make the day of the week negative. For example, if startDay is 5
     * and startDayOfWeek is -MONDAY, this indicates the first Monday on or after the 5th day of the
     * startMonth.
     *
     * <p>Day of week before day of month. To specify the last day of the week occurring on or
     * before an exact day of the month, make the day of the week and the day of the month negative.
     * For example, if startDay is -21 and startDayOfWeek is -WEDNESDAY, this indicates the last
     * Wednesday on or before the 21st of the startMonth.
     *
     * <p>The above examples refer to the startMonth, startDay, and startDayOfWeek; the same applies
     * for the endMonth, endDay, and endDayOfWeek.
     *
     * @param rawOffset The given base time zone offset to GMT.
     * @param ID The time zone ID which is obtained from TimeZone.getAvailableIDs.
     * @param startMonth The daylight savings starting month. Month is 0-based. eg, 0 for January.
     * @param startDay The daylight savings starting day-of-week-in-month. Please see the member
     *     description for an example.
     * @param startDayOfWeek The daylight savings starting day-of-week. Please see the member
     *     description for an example.
     * @param startTime The daylight savings starting time in local wall time, which is standard
     *     time in this case. Please see the member description for an example.
     * @param endMonth The daylight savings ending month. Month is 0-based. eg, 0 for January.
     * @param endDay The daylight savings ending day-of-week-in-month. Please see the member
     *     description for an example.
     * @param endDayOfWeek The daylight savings ending day-of-week. Please see the member
     *     description for an example.
     * @param endTime The daylight savings ending time in local wall time, which is daylight time in
     *     this case. Please see the member description for an example.
     * @throws IllegalArgumentException the month, day, dayOfWeek, or time parameters are out of
     *     range for the start or end rule
     * @stable ICU 2.0
     */
    public SimpleTimeZone(
            int rawOffset,
            String ID,
            int startMonth,
            int startDay,
            int startDayOfWeek,
            int startTime,
            int endMonth,
            int endDay,
            int endDayOfWeek,
            int endTime) {
        super(ID);
        construct(
                rawOffset,
                startMonth,
                startDay,
                startDayOfWeek,
                startTime,
                WALL_TIME,
                endMonth,
                endDay,
                endDayOfWeek,
                endTime,
                WALL_TIME,
                Grego.MILLIS_PER_HOUR);
    }

    /**
     * Constructs a SimpleTimeZone with the given base time zone offset from GMT, time zone ID, time
     * and its mode to start and end the daylight time. The mode specifies either {@link #WALL_TIME}
     * or {@link #STANDARD_TIME} or {@link #UTC_TIME}.
     *
     * @param rawOffset The given base time zone offset to GMT.
     * @param ID The time zone ID which is obtained from TimeZone.getAvailableIDs.
     * @param startMonth The daylight savings starting month. Month is 0-based. eg, 0 for January.
     * @param startDay The daylight savings starting day-of-week-in-month. Please see the member
     *     description for an example.
     * @param startDayOfWeek The daylight savings starting day-of-week. Please see the member
     *     description for an example.
     * @param startTime The daylight savings starting time in local wall time, which is standard
     *     time in this case. Please see the member description for an example.
     * @param startTimeMode The mode of the start time specified by startTime.
     * @param endMonth The daylight savings ending month. Month is 0-based. eg, 0 for January.
     * @param endDay The daylight savings ending day-of-week-in-month. Please see the member
     *     description for an example.
     * @param endDayOfWeek The daylight savings ending day-of-week. Please see the member
     *     description for an example.
     * @param endTime The daylight savings ending time in local wall time, which is daylight time in
     *     this case. Please see the member description for an example.
     * @param endTimeMode The mode of the end time specified by endTime.
     * @param dstSavings The amount of time in ms saved during DST.
     * @throws IllegalArgumentException the month, day, dayOfWeek, or time parameters are out of
     *     range for the start or end rule
     * @stable ICU 3.8
     */
    public SimpleTimeZone(
            int rawOffset,
            String ID,
            int startMonth,
            int startDay,
            int startDayOfWeek,
            int startTime,
            int startTimeMode,
            int endMonth,
            int endDay,
            int endDayOfWeek,
            int endTime,
            int endTimeMode,
            int dstSavings) {
        super(ID);
        construct(
                rawOffset,
                startMonth,
                startDay,
                startDayOfWeek,
                startTime,
                startTimeMode,
                endMonth,
                endDay,
                endDayOfWeek,
                endTime,
                endTimeMode,
                dstSavings);
    }

    /**
     * Constructor. This constructor is identical to the 10-argument constructor, but also takes a
     * dstSavings parameter.
     *
     * @param rawOffset The given base time zone offset to GMT.
     * @param ID The time zone ID which is obtained from TimeZone.getAvailableIDs.
     * @param startMonth The daylight savings starting month. Month is 0-based. eg, 0 for January.
     * @param startDay The daylight savings starting day-of-week-in-month. Please see the member
     *     description for an example.
     * @param startDayOfWeek The daylight savings starting day-of-week. Please see the member
     *     description for an example.
     * @param startTime The daylight savings starting time in local wall time, which is standard
     *     time in this case. Please see the member description for an example.
     * @param endMonth The daylight savings ending month. Month is 0-based. eg, 0 for January.
     * @param endDay The daylight savings ending day-of-week-in-month. Please see the member
     *     description for an example.
     * @param endDayOfWeek The daylight savings ending day-of-week. Please see the member
     *     description for an example.
     * @param endTime The daylight savings ending time in local wall time, which is daylight time in
     *     this case. Please see the member description for an example.
     * @param dstSavings The amount of time in ms saved during DST.
     * @throws IllegalArgumentException the month, day, dayOfWeek, or time parameters are out of
     *     range for the start or end rule
     * @stable ICU 2.0
     */
    public SimpleTimeZone(
            int rawOffset,
            String ID,
            int startMonth,
            int startDay,
            int startDayOfWeek,
            int startTime,
            int endMonth,
            int endDay,
            int endDayOfWeek,
            int endTime,
            int dstSavings) {
        super(ID);
        construct(
                rawOffset,
                startMonth,
                startDay,
                startDayOfWeek,
                startTime,
                WALL_TIME,
                endMonth,
                endDay,
                endDayOfWeek,
                endTime,
                WALL_TIME,
                dstSavings);
    }

    /**
     * {@inheritDoc}
     *
     * @stable ICU 3.8
     */
    @Override
    public void setID(String ID) {
        if (isFrozen()) {
            throw new UnsupportedOperationException(
                    "Attempt to modify a frozen SimpleTimeZone instance.");
        }
        super.setID(ID);
        transitionRulesInitialized = false;
    }

    /**
     * Overrides TimeZone Sets the base time zone offset to GMT. This is the offset to add "to" UTC
     * to get local time.
     *
     * @param offsetMillis the raw offset of the time zone
     * @stable ICU 2.0
     */
    @Override
    public void setRawOffset(int offsetMillis) {
        if (isFrozen()) {
            throw new UnsupportedOperationException(
                    "Attempt to modify a frozen SimpleTimeZone instance.");
        }

        raw = offsetMillis;
        transitionRulesInitialized = false;
    }

    /**
     * Overrides TimeZone Gets the GMT offset for this time zone.
     *
     * @return the raw offset
     * @stable ICU 2.0
     */
    @Override
    public int getRawOffset() {
        return raw;
    }

    /**
     * Sets the daylight savings starting year.
     *
     * @param year The daylight savings starting year.
     * @stable ICU 2.0
     */
    public void setStartYear(int year) {
        if (isFrozen()) {
            throw new UnsupportedOperationException(
                    "Attempt to modify a frozen SimpleTimeZone instance.");
        }

        getSTZInfo().sy = year;
        this.startYear = year;
        transitionRulesInitialized = false;
    }

    /**
     * Sets the daylight savings starting rule. For example, Daylight Savings Time starts at the
     * second Sunday in March, at 2 AM in standard time. Therefore, you can set the start rule by
     * calling: setStartRule(Calendar.MARCH, 2, Calendar.SUNDAY, 2*60*60*1000);
     *
     * @param month The daylight savings starting month. Month is 0-based. eg, 0 for January.
     * @param dayOfWeekInMonth The daylight savings starting day-of-week-in-month. Please see the
     *     member description for an example.
     * @param dayOfWeek The daylight savings starting day-of-week. Please see the member description
     *     for an example.
     * @param time The daylight savings starting time in local wall time, which is standard time in
     *     this case. Please see the member description for an example.
     * @throws IllegalArgumentException the month, dayOfWeekInMonth, dayOfWeek, or time parameters
     *     are out of range
     * @stable ICU 2.0
     */
    public void setStartRule(int month, int dayOfWeekInMonth, int dayOfWeek, int time) {
        if (isFrozen()) {
            throw new UnsupportedOperationException(
                    "Attempt to modify a frozen SimpleTimeZone instance.");
        }

        getSTZInfo().setStart(month, dayOfWeekInMonth, dayOfWeek, time, -1, false);
        setStartRule(month, dayOfWeekInMonth, dayOfWeek, time, WALL_TIME);
    }

    /**
     * Sets the daylight savings starting rule. For example, in the U.S., Daylight Savings Time
     * starts at the second Sunday in March, at 2 AM in standard time. Therefore, you can set the
     * start rule by calling: <code>setStartRule(Calendar.MARCH, 2, Calendar.SUNDAY, 2*60*60*1000);
     * </code> The dayOfWeekInMonth and dayOfWeek parameters together specify how to calculate the
     * exact starting date. Their exact meaning depend on their respective signs, allowing various
     * types of rules to be constructed, as follows:
     *
     * <ul>
     *   <li>If both dayOfWeekInMonth and dayOfWeek are positive, they specify the day of week in
     *       the month (e.g., (2, WEDNESDAY) is the second Wednesday of the month).
     *   <li>If dayOfWeek is positive and dayOfWeekInMonth is negative, they specify the day of week
     *       in the month counting backward from the end of the month. (e.g., (-1, MONDAY) is the
     *       last Monday in the month)
     *   <li>If dayOfWeek is zero and dayOfWeekInMonth is positive, dayOfWeekInMonth specifies the
     *       day of the month, regardless of what day of the week it is. (e.g., (10, 0) is the tenth
     *       day of the month)
     *   <li>If dayOfWeek is zero and dayOfWeekInMonth is negative, dayOfWeekInMonth specifies the
     *       day of the month counting backward from the end of the month, regardless of what day of
     *       the week it is (e.g., (-2, 0) is the next-to-last day of the month).
     *   <li>If dayOfWeek is negative and dayOfWeekInMonth is positive, they specify the first
     *       specified day of the week on or after the specified day of the month. (e.g., (15,
     *       -SUNDAY) is the first Sunday after the 15th of the month [or the 15th itself if the
     *       15th is a Sunday].)
     *   <li>If dayOfWeek and DayOfWeekInMonth are both negative, they specify the last specified
     *       day of the week on or before the specified day of the month. (e.g., (-20, -TUESDAY) is
     *       the last Tuesday before the 20th of the month [or the 20th itself if the 20th is a
     *       Tuesday].)
     * </ul>
     *
     * @param month the daylight savings starting month. Month is 0-based. eg, 0 for January.
     * @param dayOfWeekInMonth the daylight savings starting day-of-week-in-month. Please see the
     *     member description for an example.
     * @param dayOfWeek the daylight savings starting day-of-week. Please see the member description
     *     for an example.
     * @param time the daylight savings starting time. Please see the member description for an
     *     example.
     */
    private void setStartRule(int month, int dayOfWeekInMonth, int dayOfWeek, int time, int mode) {
        assert (!isFrozen());

        startMonth = month;
        startDay = dayOfWeekInMonth;
        startDayOfWeek = dayOfWeek;
        startTime = time;
        startTimeMode = mode;
        decodeStartRule();

        transitionRulesInitialized = false;
    }

    /**
     * Sets the DST start rule to a fixed date within a month.
     *
     * @param month The month in which this rule occurs (0-based).
     * @param dayOfMonth The date in that month (1-based).
     * @param time The time of that day (number of millis after midnight) when DST takes effect in
     *     local wall time, which is standard time in this case.
     * @throws IllegalArgumentException the month, dayOfMonth, or time parameters are out of range
     * @stable ICU 2.0
     */
    public void setStartRule(int month, int dayOfMonth, int time) {
        if (isFrozen()) {
            throw new UnsupportedOperationException(
                    "Attempt to modify a frozen SimpleTimeZone instance.");
        }

        getSTZInfo().setStart(month, -1, -1, time, dayOfMonth, false);
        setStartRule(month, dayOfMonth, 0, time, WALL_TIME);
    }

    /**
     * Sets the DST start rule to a weekday before or after a give date within a month, e.g., the
     * first Monday on or after the 8th.
     *
     * @param month The month in which this rule occurs (0-based).
     * @param dayOfMonth A date within that month (1-based).
     * @param dayOfWeek The day of the week on which this rule occurs.
     * @param time The time of that day (number of millis after midnight) when DST takes effect in
     *     local wall time, which is standard time in this case.
     * @param after If true, this rule selects the first dayOfWeek on or after dayOfMonth. If false,
     *     this rule selects the last dayOfWeek on or before dayOfMonth.
     * @throws IllegalArgumentException the month, dayOfMonth, dayOfWeek, or time parameters are out
     *     of range
     * @stable ICU 2.0
     */
    public void setStartRule(int month, int dayOfMonth, int dayOfWeek, int time, boolean after) {
        if (isFrozen()) {
            throw new UnsupportedOperationException(
                    "Attempt to modify a frozen SimpleTimeZone instance.");
        }

        getSTZInfo().setStart(month, -1, dayOfWeek, time, dayOfMonth, after);
        setStartRule(month, after ? dayOfMonth : -dayOfMonth, -dayOfWeek, time, WALL_TIME);
    }

    /**
     * Sets the daylight savings ending rule. For example, if Daylight Savings Time ends at the last
     * (-1) Sunday in October, at 2 AM in standard time, you can set the end rule by calling: <code>
     * setEndRule(Calendar.OCTOBER, -1, Calendar.SUNDAY, 2*60*60*1000);</code>
     *
     * @param month The daylight savings ending month. Month is 0-based. eg, 0 for January.
     * @param dayOfWeekInMonth The daylight savings ending day-of-week-in-month. Please see the
     *     member description for an example.
     * @param dayOfWeek The daylight savings ending day-of-week. Please see the member description
     *     for an example.
     * @param time The daylight savings ending time in local wall time, which is daylight time in
     *     this case. Please see the member description for an example.
     * @throws IllegalArgumentException the month, dayOfWeekInMonth, dayOfWeek, or time parameters
     *     are out of range
     * @stable ICU 2.0
     */
    public void setEndRule(int month, int dayOfWeekInMonth, int dayOfWeek, int time) {
        if (isFrozen()) {
            throw new UnsupportedOperationException(
                    "Attempt to modify a frozen SimpleTimeZone instance.");
        }

        getSTZInfo().setEnd(month, dayOfWeekInMonth, dayOfWeek, time, -1, false);
        setEndRule(month, dayOfWeekInMonth, dayOfWeek, time, WALL_TIME);
    }

    /**
     * Sets the DST end rule to a fixed date within a month.
     *
     * @param month The month in which this rule occurs (0-based).
     * @param dayOfMonth The date in that month (1-based).
     * @param time The time of that day (number of millis after midnight) when DST ends in local
     *     wall time, which is daylight time in this case.
     * @throws IllegalArgumentException the month, dayOfMonth, or time parameters are out of range
     * @stable ICU 2.0
     */
    public void setEndRule(int month, int dayOfMonth, int time) {
        if (isFrozen()) {
            throw new UnsupportedOperationException(
                    "Attempt to modify a frozen SimpleTimeZone instance.");
        }

        getSTZInfo().setEnd(month, -1, -1, time, dayOfMonth, false);
        setEndRule(month, dayOfMonth, WALL_TIME, time);
    }

    /**
     * Sets the DST end rule to a weekday before or after a give date within a month, e.g., the
     * first Monday on or after the 8th.
     *
     * @param month The month in which this rule occurs (0-based).
     * @param dayOfMonth A date within that month (1-based).
     * @param dayOfWeek The day of the week on which this rule occurs.
     * @param time The time of that day (number of millis after midnight) when DST ends in local
     *     wall time, which is daylight time in this case.
     * @param after If true, this rule selects the first dayOfWeek on or after dayOfMonth. If false,
     *     this rule selects the last dayOfWeek on or before dayOfMonth.
     * @throws IllegalArgumentException the month, dayOfMonth, dayOfWeek, or time parameters are out
     *     of range
     * @stable ICU 2.0
     */
    public void setEndRule(int month, int dayOfMonth, int dayOfWeek, int time, boolean after) {
        if (isFrozen()) {
            throw new UnsupportedOperationException(
                    "Attempt to modify a frozen SimpleTimeZone instance.");
        }

        getSTZInfo().setEnd(month, -1, dayOfWeek, time, dayOfMonth, after);
        setEndRule(month, dayOfMonth, dayOfWeek, time, WALL_TIME, after);
    }

    private void setEndRule(
            int month, int dayOfMonth, int dayOfWeek, int time, int mode, boolean after) {
        assert (!isFrozen());
        setEndRule(month, after ? dayOfMonth : -dayOfMonth, -dayOfWeek, time, mode);
    }

    /**
     * Sets the daylight savings ending rule. For example, in the U.S., Daylight Savings Time ends
     * at the first Sunday in November, at 2 AM in standard time. Therefore, you can set the end
     * rule by calling: setEndRule(Calendar.NOVEMBER, 1, Calendar.SUNDAY, 2*60*60*1000); Various
     * other types of rules can be specified by manipulating the dayOfWeek and dayOfWeekInMonth
     * parameters. For complete details, see the documentation for setStartRule().
     *
     * @param month the daylight savings ending month. Month is 0-based. eg, 0 for January.
     * @param dayOfWeekInMonth the daylight savings ending day-of-week-in-month. See setStartRule()
     *     for a complete explanation.
     * @param dayOfWeek the daylight savings ending day-of-week. See setStartRule() for a complete
     *     explanation.
     * @param time the daylight savings ending time. Please see the member description for an
     *     example.
     */
    private void setEndRule(int month, int dayOfWeekInMonth, int dayOfWeek, int time, int mode) {
        assert (!isFrozen());

        endMonth = month;
        endDay = dayOfWeekInMonth;
        endDayOfWeek = dayOfWeek;
        endTime = time;
        endTimeMode = mode;
        decodeEndRule();

        transitionRulesInitialized = false;
    }

    /**
     * Sets the amount of time in ms that the clock is advanced during DST.
     *
     * @param millisSavedDuringDST the number of milliseconds the time is advanced with respect to
     *     standard time when the daylight savings rules are in effect. Typically one hour
     *     (+3600000). The amount could be negative, but not 0.
     * @stable ICU 2.0
     */
    public void setDSTSavings(int millisSavedDuringDST) {
        if (isFrozen()) {
            throw new UnsupportedOperationException(
                    "Attempt to modify a frozen SimpleTimeZone instance.");
        }

        if (millisSavedDuringDST == 0) {
            throw new IllegalArgumentException();
        }
        dst = millisSavedDuringDST;

        transitionRulesInitialized = false;
    }

    /**
     * Returns the amount of time in ms that the clock is advanced during DST.
     *
     * @return the number of milliseconds the time is advanced with respect to standard time when
     *     the daylight savings rules are in effect. Typically one hour (3600000). The amount could
     *     be negative, but not 0.
     * @stable ICU 2.0
     */
    @Override
    public int getDSTSavings() {
        return dst;
    }

    /**
     * Returns the java.util.SimpleTimeZone that this class wraps.
     *
     * <p>java.util.SimpleTimeZone unwrapSTZ() { return (java.util.SimpleTimeZone) unwrap(); }
     */

    // on JDK 1.4 and later, can't deserialize a SimpleTimeZone as a SimpleTimeZone...
    private void readObject(java.io.ObjectInputStream in)
            throws IOException, ClassNotFoundException {
        in.defaultReadObject();
        /*
        String id = getID();
        if (id!=null && !(zone instanceof java.util.SimpleTimeZone && zone.getID().equals(id))) {
            // System.out.println("*** readjust " + zone.getClass().getName() +
            // " " + zone.getID() + " ***");
            java.util.SimpleTimeZone stz =
                new java.util.SimpleTimeZone(raw, id);
            if (dst != 0) {
                stz.setDSTSavings(dst);
                // if it is 0, then there shouldn't be start/end rules and the default
                // behavior should be no dst
            }

            if (xinfo != null) {
                xinfo.applyTo(stz);
            }
            zoneJDK = stz;
        }
        */
        /* set all instance variables in this object
         * to the values in zone
         */
        if (xinfo != null) {
            xinfo.applyTo(this);
        }
    }

    /**
     * Returns a string representation of this object.
     *
     * @return a string representation of this object
     * @stable ICU 2.0
     */
    @Override
    public String toString() {
        return "SimpleTimeZone: " + getID();
    }

    private STZInfo getSTZInfo() {
        if (xinfo == null) {
            xinfo = new STZInfo();
        }
        return xinfo;
    }

    //  Use only for decodeStartRule() and decodeEndRule() where the year is not
    //  available. Set February to 29 days to accommodate rules with that date
    //  and day-of-week-on-or-before-that-date mode (DOW_LE_DOM_MODE).
    //  The compareToRule() method adjusts to February 28 in non-leap years.
    //
    //  For actual getOffset() calculations, use TimeZone::monthLength() and
    //  TimeZone::previousMonthLength() which take leap years into account.
    //  We handle leap years assuming always
    //  Gregorian, since we know they didn't have daylight time when
    //  Gregorian calendar started.
    private static final byte staticMonthLength[] = {
        31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31
    };

    /**
     * {@inheritDoc}
     *
     * @stable ICU 2.0
     */
    @Override
    public int getOffset(int era, int year, int month, int day, int dayOfWeek, int millis) {
        // Check the month before calling Grego.monthLength(). This
        // duplicates the test that occurs in the 7-argument getOffset(),
        // however, this is unavoidable. We don't mind because this method, in
        // fact, should not be called; internal code should always call the
        // 7-argument getOffset(), and outside code should use Calendar.get(int
        // field) with fields ZONE_OFFSET and DST_OFFSET. We can't get rid of
        // this method because it's public API. - liu 8/10/98
        if (month < Calendar.JANUARY || month > Calendar.DECEMBER) {
            throw new IllegalArgumentException();
        }

        return getOffset(era, year, month, day, dayOfWeek, millis, Grego.monthLength(year, month));
    }

    /**
     * @internal
     * @deprecated This API is ICU internal only.
     */
    @Deprecated
    public int getOffset(
            int era, int year, int month, int day, int dayOfWeek, int millis, int monthLength) {
        // Check the month before calling Grego.monthLength(). This
        // duplicates a test that occurs in the 9-argument getOffset(),
        // however, this is unavoidable. We don't mind because this method, in
        // fact, should not be called; internal code should always call the
        // 9-argument getOffset(), and outside code should use Calendar.get(int
        // field) with fields ZONE_OFFSET and DST_OFFSET. We can't get rid of
        // this method because it's public API. - liu 8/10/98
        if (month < Calendar.JANUARY || month > Calendar.DECEMBER) {
            throw new IllegalArgumentException();
        }

        return getOffset(
                era,
                year,
                month,
                day,
                dayOfWeek,
                millis,
                Grego.monthLength(year, month),
                Grego.previousMonthLength(year, month));
    }

    private int getOffset(
            int era,
            int year,
            int month,
            int day,
            int dayOfWeek,
            int millis,
            int monthLength,
            int prevMonthLength) {

        if (true) {
            /* Use this parameter checking code for normal operation.  Only one
             * of these two blocks should actually get compiled into the class
             * file.  */
            if ((era != GregorianCalendar.AD && era != GregorianCalendar.BC)
                    || month < Calendar.JANUARY
                    || month > Calendar.DECEMBER
                    || day < 1
                    || day > monthLength
                    || dayOfWeek < Calendar.SUNDAY
                    || dayOfWeek > Calendar.SATURDAY
                    || millis < 0
                    || millis >= Grego.MILLIS_PER_DAY
                    || monthLength < 28
                    || monthLength > 31
                    || prevMonthLength < 28
                    || prevMonthLength > 31) {
                throw new IllegalArgumentException();
            }
        }
        // Eclipse stated the following is "dead code"
        /*else {
            // This parameter checking code is better for debugging, but
            // overkill for normal operation.  Only one of these two blocks
            // should actually get compiled into the class file.
            if (era != GregorianCalendar.AD && era != GregorianCalendar.BC) {
                throw new IllegalArgumentException("Illegal era " + era);
            }
            if (month < Calendar.JANUARY
                || month > Calendar.DECEMBER) {
                throw new IllegalArgumentException("Illegal month " + month);
            }
            if (day < 1
                || day > monthLength) {
                throw new IllegalArgumentException("Illegal day " + day+" max month len: "+monthLength);
            }
            if (dayOfWeek < Calendar.SUNDAY
                || dayOfWeek > Calendar.SATURDAY) {
                throw new IllegalArgumentException("Illegal day of week " + dayOfWeek);
            }
            if (millis < 0
                || millis >= Grego.MILLIS_PER_DAY) {
                throw new IllegalArgumentException("Illegal millis " + millis);
            }
            if (monthLength < 28
                || monthLength > 31) {
                throw new IllegalArgumentException("Illegal month length " + monthLength);
            }
            if (prevMonthLength < 28
                || prevMonthLength > 31) {
                throw new IllegalArgumentException("Illegal previous month length " + prevMonthLength);
            }
        }*/

        int result = raw;

        // Bail out if we are before the onset of daylight savings time
        if (!useDaylight || year < startYear || era != GregorianCalendar.AD) return result;

        // Check for southern hemisphere.  We assume that the start and end
        // month are different.
        boolean southern = (startMonth > endMonth);

        // Compare the date to the starting and ending rules.+1 = date>rule, -1
        // = date<rule, 0 = date==rule.
        int startCompare =
                compareToRule(
                        month,
                        monthLength,
                        prevMonthLength,
                        day,
                        dayOfWeek,
                        millis,
                        startTimeMode == UTC_TIME ? -raw : 0,
                        startMode,
                        startMonth,
                        startDayOfWeek,
                        startDay,
                        startTime);
        int endCompare = 0;

        /* We don't always have to compute endCompare.  For many instances,
         * startCompare is enough to determine if we are in DST or not.  In the
         * northern hemisphere, if we are before the start rule, we can't have
         * DST.  In the southern hemisphere, if we are after the start rule, we
         * must have DST.  This is reflected in the way the next if statement
         * (not the one immediately following) short circuits. */
        if (southern != (startCompare >= 0)) {
            /* For the ending rule comparison, we add the dstSavings to the millis
             * passed in to convert them from standard to wall time.  We then must
             * normalize the millis to the range 0..millisPerDay-1. */
            endCompare =
                    compareToRule(
                            month,
                            monthLength,
                            prevMonthLength,
                            day,
                            dayOfWeek,
                            millis,
                            endTimeMode == WALL_TIME ? dst : (endTimeMode == UTC_TIME ? -raw : 0),
                            endMode,
                            endMonth,
                            endDayOfWeek,
                            endDay,
                            endTime);
        }

        // Check for both the northern and southern hemisphere cases.  We
        // assume that in the northern hemisphere, the start rule is before the
        // end rule within the calendar year, and vice versa for the southern
        // hemisphere.
        if ((!southern && (startCompare >= 0 && endCompare < 0))
                || (southern && (startCompare >= 0 || endCompare < 0))) result += dst;

        return result;
    }

    /**
     * {@inheritDoc}
     *
     * @stable ICU 69
     */
    @Override
    public void getOffsetFromLocal(
            long date,
            LocalOption nonExistingTimeOpt,
            LocalOption duplicatedTimeOpt,
            int[] offsets) {
        int nonExistingTimeOptVal = getLocalOptionValue(nonExistingTimeOpt);
        int duplicatedTimeOptVal = getLocalOptionValue(duplicatedTimeOpt);

        offsets[0] = getRawOffset();
        int fields[] = new int[6];
        Grego.timeToFields(date, fields);
        offsets[1] =
                getOffset(
                                GregorianCalendar.AD,
                                fields[0],
                                fields[1],
                                fields[2],
                                fields[3],
                                fields[5])
                        - offsets[0];

        boolean recalc = false;

        // Now, we need some adjustment
        if (offsets[1] > 0) {
            if ((nonExistingTimeOptVal & STD_DST_MASK) == LOCAL_STD
                    || (nonExistingTimeOptVal & STD_DST_MASK) != LOCAL_DST
                            && (nonExistingTimeOptVal & FORMER_LATTER_MASK) != LOCAL_LATTER) {
                date -= getDSTSavings();
                recalc = true;
            }
        } else {
            if ((duplicatedTimeOptVal & STD_DST_MASK) == LOCAL_DST
                    || (duplicatedTimeOptVal & STD_DST_MASK) != LOCAL_STD
                            && (duplicatedTimeOptVal & FORMER_LATTER_MASK) == LOCAL_FORMER) {
                date -= getDSTSavings();
                recalc = true;
            }
        }

        if (recalc) {
            Grego.timeToFields(date, fields);
            offsets[1] =
                    getOffset(
                                    GregorianCalendar.AD,
                                    fields[0],
                                    fields[1],
                                    fields[2],
                                    fields[3],
                                    fields[5])
                            - offsets[0];
        }
    }

    private static final int DOM_MODE = 1,
            DOW_IN_MONTH_MODE = 2,
            DOW_GE_DOM_MODE = 3,
            DOW_LE_DOM_MODE = 4;

    /**
     * Compare a given date in the year to a rule. Return 1, 0, or -1, depending on whether the date
     * is after, equal to, or before the rule date. The millis are compared directly against the
     * ruleMillis, so any standard-daylight adjustments must be handled by the caller.
     *
     * @return 1 if the date is after the rule date, -1 if the date is before the rule date, or 0 if
     *     the date is equal to the rule date.
     */
    private int compareToRule(
            int month,
            int monthLen,
            int prevMonthLen,
            int dayOfMonth,
            int dayOfWeek,
            int millis,
            int millisDelta,
            int ruleMode,
            int ruleMonth,
            int ruleDayOfWeek,
            int ruleDay,
            int ruleMillis) {
        // Make adjustments for startTimeMode and endTimeMode

        millis += millisDelta;

        while (millis >= Grego.MILLIS_PER_DAY) {
            millis -= Grego.MILLIS_PER_DAY;
            ++dayOfMonth;
            dayOfWeek = 1 + (dayOfWeek % 7); // dayOfWeek is one-based
            if (dayOfMonth > monthLen) {
                dayOfMonth = 1;
                /* When incrementing the month, it is desirable to overflow
                 * from DECEMBER to DECEMBER+1, since we use the result to
                 * compare against a real month. Wraparound of the value
                 * leads to bug 4173604. */
                ++month;
            }
        }
        /*
         * For some reasons, Sun Java 6 on Solaris/Linux has a problem with
         * the while loop below (at least Java 6 up to build 1.6.0_02-b08).
         * It looks the JRE messes up the variable 'millis' while executing
         * the code in the while block.  The problem is not reproduced with
         * JVM option -Xint, that is, it is likely a bug of the HotSpot
         * adaptive compiler.  Moving 'millis += Grego.MILLIS_PER_DAY'
         * to the end of this while block seems to resolve the problem.
         * See ticket#5887 about the problem in detail.
         */
        while (millis < 0) {
            // millis += Grego.MILLIS_PER_DAY;
            --dayOfMonth;
            dayOfWeek = 1 + ((dayOfWeek + 5) % 7); // dayOfWeek is one-based
            if (dayOfMonth < 1) {
                dayOfMonth = prevMonthLen;
                --month;
            }
            millis += Grego.MILLIS_PER_DAY;
        }

        if (month < ruleMonth) return -1;
        else if (month > ruleMonth) return 1;

        int ruleDayOfMonth = 0;

        // Adjust the ruleDay to the monthLen, for non-leap year February 29 rule days.
        if (ruleDay > monthLen) {
            ruleDay = monthLen;
        }

        switch (ruleMode) {
            case DOM_MODE:
                ruleDayOfMonth = ruleDay;
                break;
            case DOW_IN_MONTH_MODE:
                // In this case ruleDay is the day-of-week-in-month
                if (ruleDay > 0)
                    ruleDayOfMonth =
                            1
                                    + (ruleDay - 1) * 7
                                    + (7 + ruleDayOfWeek - (dayOfWeek - dayOfMonth + 1)) % 7;
                else // Assume ruleDay < 0 here
                {
                    ruleDayOfMonth =
                            monthLen
                                    + (ruleDay + 1) * 7
                                    - (7 + (dayOfWeek + monthLen - dayOfMonth) - ruleDayOfWeek) % 7;
                }
                break;
            case DOW_GE_DOM_MODE:
                ruleDayOfMonth =
                        ruleDay + (49 + ruleDayOfWeek - ruleDay - dayOfWeek + dayOfMonth) % 7;
                break;
            case DOW_LE_DOM_MODE:
                ruleDayOfMonth =
                        ruleDay - (49 - ruleDayOfWeek + ruleDay + dayOfWeek - dayOfMonth) % 7;
                // Note at this point ruleDayOfMonth may be <1, although it will
                // be >=1 for well-formed rules.
                break;
        }

        if (dayOfMonth < ruleDayOfMonth) return -1;
        else if (dayOfMonth > ruleDayOfMonth) return 1;

        if (millis < ruleMillis) {
            return -1;
        } else if (millis > ruleMillis) {
            return 1;
        } else {
            return 0;
        }
    }

    // data needed for streaming mutated SimpleTimeZones in JDK14
    private int raw; // the TimeZone's raw GMT offset
    private int dst = 3600000;
    private STZInfo xinfo = null;
    private int startMonth, startDay, startDayOfWeek; // the month, day, DOW, and time DST starts
    private int startTime;
    private int startTimeMode, endTimeMode; // Mode for startTime, endTime; see TimeMode
    private int endMonth, endDay, endDayOfWeek; // the month, day, DOW, and time DST ends
    private int endTime;
    private int startYear; // the year these DST rules took effect
    private boolean useDaylight; // flag indicating whether this TimeZone uses DST
    private int startMode, endMode; // flags indicating what kind of rules the DST rules are

    /**
     * Overrides TimeZone Queries if this time zone uses Daylight Saving Time.
     *
     * @stable ICU 2.0
     */
    @Override
    public boolean useDaylightTime() {
        return useDaylight;
    }

    /**
     * {@inheritDoc}
     *
     * @stable ICU 49
     */
    @Override
    public boolean observesDaylightTime() {
        return useDaylight;
    }

    /**
     * Overrides TimeZone Queries if the give date is in Daylight Saving Time.
     *
     * @stable ICU 2.0
     */
    @Override
    public boolean inDaylightTime(Date date) {
        GregorianCalendar gc = new GregorianCalendar(this);
        gc.setTime(date);
        return gc.inDaylightTime();
    }

    /** Internal construction method. */
    private void construct(
            int _raw,
            int _startMonth,
            int _startDay,
            int _startDayOfWeek,
            int _startTime,
            int _startTimeMode,
            int _endMonth,
            int _endDay,
            int _endDayOfWeek,
            int _endTime,
            int _endTimeMode,
            int _dst) {
        raw = _raw;
        startMonth = _startMonth;
        startDay = _startDay;
        startDayOfWeek = _startDayOfWeek;
        startTime = _startTime;
        startTimeMode = _startTimeMode;
        endMonth = _endMonth;
        endDay = _endDay;
        endDayOfWeek = _endDayOfWeek;
        endTime = _endTime;
        endTimeMode = _endTimeMode;
        dst = _dst;
        startYear = 0;
        startMode = DOM_MODE;
        endMode = DOM_MODE;

        decodeRules();

        if (_dst == 0) {
            throw new IllegalArgumentException();
        }
    }

    private void decodeRules() {
        decodeStartRule();
        decodeEndRule();
    }

    /**
     * Decode the start rule and validate the parameters. The parameters are expected to be in
     * encoded form, which represents the various rule modes by negating or zeroing certain values.
     * Representation formats are:
     *
     * <p>
     *
     * <pre>
     *            DOW_IN_MONTH  DOM    DOW>=DOM  DOW<=DOM  no DST
     *            ------------  -----  --------  --------  ----------
     * month       0..11        same    same      same     don't care
     * day        -5..5         1..31   1..31    -1..-31   0
     * dayOfWeek   1..7         0      -1..-7    -1..-7    don't care
     * time        0..ONEDAY    same    same      same     don't care
     * </pre>
     *
     * The range for month does not include UNDECIMBER since this class is really specific to
     * GregorianCalendar, which does not use that month. The range for time includes ONEDAY (vs.
     * ending at ONEDAY-1) because the end rule is an exclusive limit point. That is, the range of
     * times that are in DST include those >= the start and < the end. For this reason, it should be
     * possible to specify an end of ONEDAY in order to include the entire day. Although this is
     * equivalent to time 0 of the following day, it's not always possible to specify that, for
     * example, on December 31. While arguably the start range should still be 0..ONEDAY-1, we keep
     * the start and end ranges the same for consistency.
     */
    private void decodeStartRule() {
        useDaylight = (startDay != 0) && (endDay != 0);
        if (useDaylight && dst == 0) {
            dst = Grego.MILLIS_PER_DAY;
        }
        if (startDay != 0) {
            if (startMonth < Calendar.JANUARY || startMonth > Calendar.DECEMBER) {
                throw new IllegalArgumentException();
            }
            if (startTime < 0
                    || startTime > Grego.MILLIS_PER_DAY
                    || startTimeMode < WALL_TIME
                    || startTimeMode > UTC_TIME) {
                throw new IllegalArgumentException();
            }
            if (startDayOfWeek == 0) {
                startMode = DOM_MODE;
            } else {
                if (startDayOfWeek > 0) {
                    startMode = DOW_IN_MONTH_MODE;
                } else {
                    startDayOfWeek = -startDayOfWeek;
                    if (startDay > 0) {
                        startMode = DOW_GE_DOM_MODE;
                    } else {
                        startDay = -startDay;
                        startMode = DOW_LE_DOM_MODE;
                    }
                }
                if (startDayOfWeek > Calendar.SATURDAY) {
                    throw new IllegalArgumentException();
                }
            }
            if (startMode == DOW_IN_MONTH_MODE) {
                if (startDay < -5 || startDay > 5) {
                    throw new IllegalArgumentException();
                }
            } else if (startDay < 1 || startDay > staticMonthLength[startMonth]) {
                throw new IllegalArgumentException();
            }
        }
    }

    /**
     * Decode the end rule and validate the parameters. This method is exactly analogous to
     * decodeStartRule().
     *
     * @see #decodeStartRule
     */
    private void decodeEndRule() {
        useDaylight = (startDay != 0) && (endDay != 0);
        if (useDaylight && dst == 0) {
            dst = Grego.MILLIS_PER_DAY;
        }
        if (endDay != 0) {
            if (endMonth < Calendar.JANUARY || endMonth > Calendar.DECEMBER) {
                throw new IllegalArgumentException();
            }
            if (endTime < 0
                    || endTime > Grego.MILLIS_PER_DAY
                    || endTimeMode < WALL_TIME
                    || endTimeMode > UTC_TIME) {
                throw new IllegalArgumentException();
            }
            if (endDayOfWeek == 0) {
                endMode = DOM_MODE;
            } else {
                if (endDayOfWeek > 0) {
                    endMode = DOW_IN_MONTH_MODE;
                } else {
                    endDayOfWeek = -endDayOfWeek;
                    if (endDay > 0) {
                        endMode = DOW_GE_DOM_MODE;
                    } else {
                        endDay = -endDay;
                        endMode = DOW_LE_DOM_MODE;
                    }
                }
                if (endDayOfWeek > Calendar.SATURDAY) {
                    throw new IllegalArgumentException();
                }
            }
            if (endMode == DOW_IN_MONTH_MODE) {
                if (endDay < -5 || endDay > 5) {
                    throw new IllegalArgumentException();
                }
            } else if (endDay < 1 || endDay > staticMonthLength[endMonth]) {
                throw new IllegalArgumentException();
            }
        }
    }

    /**
     * Overrides equals.
     *
     * @return true if obj is a SimpleTimeZone equivalent to this
     * @stable ICU 2.0
     */
    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null || getClass() != obj.getClass()) return false;
        SimpleTimeZone that = (SimpleTimeZone) obj;
        return raw == that.raw
                && useDaylight == that.useDaylight
                && idEquals(getID(), that.getID())
                && (!useDaylight
                        // Only check rules if using DST
                        || (dst == that.dst
                                && startMode == that.startMode
                                && startMonth == that.startMonth
                                && startDay == that.startDay
                                && startDayOfWeek == that.startDayOfWeek
                                && startTime == that.startTime
                                && startTimeMode == that.startTimeMode
                                && endMode == that.endMode
                                && endMonth == that.endMonth
                                && endDay == that.endDay
                                && endDayOfWeek == that.endDayOfWeek
                                && endTime == that.endTime
                                && endTimeMode == that.endTimeMode
                                && startYear == that.startYear));
    }

    private boolean idEquals(String id1, String id2) {
        if (id1 == null && id2 == null) {
            return true;
        }
        if (id1 != null && id2 != null) {
            return id1.equals(id2);
        }
        return false;
    }

    /**
     * Overrides hashCode.
     *
     * @return a hash code value for this object.
     * @stable ICU 2.0
     */
    @Override
    public int hashCode() {
        int ret = super.hashCode() + raw ^ (raw >>> 8) + (useDaylight ? 0 : 1);
        if (!useDaylight) {
            ret +=
                    dst
                            ^ (dst >>> 10) + startMode
                            ^ (startMode >>> 11) + startMonth
                            ^ (startMonth >>> 12) + startDay
                            ^ (startDay >>> 13) + startDayOfWeek
                            ^ (startDayOfWeek >>> 14) + startTime
                            ^ (startTime >>> 15) + startTimeMode
                            ^ (startTimeMode >>> 16) + endMode
                            ^ (endMode >>> 17) + endMonth
                            ^ (endMonth >>> 18) + endDay
                            ^ (endDay >>> 19) + endDayOfWeek
                            ^ (endDayOfWeek >>> 20) + endTime
                            ^ (endTime >>> 21) + endTimeMode
                            ^ (endTimeMode >>> 22) + startYear
                            ^ (startYear >>> 23);
        }
        return ret;
    }

    /**
     * Overrides clone.
     *
     * @stable ICU 2.0
     */
    @Override
    public SimpleTimeZone clone() {
        if (isFrozen()) {
            return this;
        }
        return cloneAsThawed();
    }

    /**
     * Returns true if this zone has the same rules and offset as another zone.
     *
     * @param othr the TimeZone object to be compared with
     * @return true if the given zone has the same rules and offset as this one
     * @stable ICU 2.0
     */
    @Override
    public boolean hasSameRules(TimeZone othr) {
        if (this == othr) {
            return true;
        }
        if (!(othr instanceof SimpleTimeZone)) {
            return false;
        }
        SimpleTimeZone other = (SimpleTimeZone) othr;
        return other != null
                && raw == other.raw
                && useDaylight == other.useDaylight
                && (!useDaylight
                        // Only check rules if using DST
                        || (dst == other.dst
                                && startMode == other.startMode
                                && startMonth == other.startMonth
                                && startDay == other.startDay
                                && startDayOfWeek == other.startDayOfWeek
                                && startTime == other.startTime
                                && startTimeMode == other.startTimeMode
                                && endMode == other.endMode
                                && endMonth == other.endMonth
                                && endDay == other.endDay
                                && endDayOfWeek == other.endDayOfWeek
                                && endTime == other.endTime
                                && endTimeMode == other.endTimeMode
                                && startYear == other.startYear));
    }

    // BasicTimeZone methods

    /**
     * {@inheritDoc}
     *
     * @stable ICU 3.8
     */
    @Override
    public TimeZoneTransition getNextTransition(long base, boolean inclusive) {
        if (!useDaylight) {
            return null;
        }

        initTransitionRules();
        long firstTransitionTime = firstTransition.getTime();
        if (base < firstTransitionTime || (inclusive && base == firstTransitionTime)) {
            return firstTransition;
        }
        Date stdDate =
                stdRule.getNextStart(
                        base, dstRule.getRawOffset(), dstRule.getDSTSavings(), inclusive);
        Date dstDate =
                dstRule.getNextStart(
                        base, stdRule.getRawOffset(), stdRule.getDSTSavings(), inclusive);
        if (stdDate != null && (dstDate == null || stdDate.before(dstDate))) {
            return new TimeZoneTransition(stdDate.getTime(), dstRule, stdRule);
        }
        if (dstDate != null && (stdDate == null || dstDate.before(stdDate))) {
            return new TimeZoneTransition(dstDate.getTime(), stdRule, dstRule);
        }
        return null;
    }

    /**
     * {@inheritDoc}
     *
     * @stable ICU 3.8
     */
    @Override
    public TimeZoneTransition getPreviousTransition(long base, boolean inclusive) {
        if (!useDaylight) {
            return null;
        }

        initTransitionRules();
        long firstTransitionTime = firstTransition.getTime();
        if (base < firstTransitionTime || (!inclusive && base == firstTransitionTime)) {
            return null;
        }
        Date stdDate =
                stdRule.getPreviousStart(
                        base, dstRule.getRawOffset(), dstRule.getDSTSavings(), inclusive);
        Date dstDate =
                dstRule.getPreviousStart(
                        base, stdRule.getRawOffset(), stdRule.getDSTSavings(), inclusive);
        if (stdDate != null && (dstDate == null || stdDate.after(dstDate))) {
            return new TimeZoneTransition(stdDate.getTime(), dstRule, stdRule);
        }
        if (dstDate != null && (stdDate == null || dstDate.after(stdDate))) {
            return new TimeZoneTransition(dstDate.getTime(), stdRule, dstRule);
        }
        return null;
    }

    /**
     * {@inheritDoc}
     *
     * @stable ICU 3.8
     */
    @Override
    public TimeZoneRule[] getTimeZoneRules() {
        initTransitionRules();

        int size = useDaylight ? 3 : 1;
        TimeZoneRule[] rules = new TimeZoneRule[size];
        rules[0] = initialRule;
        if (useDaylight) {
            rules[1] = stdRule;
            rules[2] = dstRule;
        }
        return rules;
    }

    private transient boolean transitionRulesInitialized;
    private transient InitialTimeZoneRule initialRule;
    private transient TimeZoneTransition firstTransition;
    private transient AnnualTimeZoneRule stdRule;
    private transient AnnualTimeZoneRule dstRule;

    private synchronized void initTransitionRules() {
        if (transitionRulesInitialized) {
            return;
        }
        if (useDaylight) {
            DateTimeRule dtRule = null;
            int timeRuleType;
            long firstStdStart, firstDstStart;

            // Create a TimeZoneRule for daylight saving time
            timeRuleType =
                    (startTimeMode == STANDARD_TIME)
                            ? DateTimeRule.STANDARD_TIME
                            : ((startTimeMode == UTC_TIME)
                                    ? DateTimeRule.UTC_TIME
                                    : DateTimeRule.WALL_TIME);
            switch (startMode) {
                case DOM_MODE:
                    dtRule = new DateTimeRule(startMonth, startDay, startTime, timeRuleType);
                    break;
                case DOW_IN_MONTH_MODE:
                    dtRule =
                            new DateTimeRule(
                                    startMonth, startDay, startDayOfWeek, startTime, timeRuleType);
                    break;
                case DOW_GE_DOM_MODE:
                    dtRule =
                            new DateTimeRule(
                                    startMonth,
                                    startDay,
                                    startDayOfWeek,
                                    true,
                                    startTime,
                                    timeRuleType);
                    break;
                case DOW_LE_DOM_MODE:
                    dtRule =
                            new DateTimeRule(
                                    startMonth,
                                    startDay,
                                    startDayOfWeek,
                                    false,
                                    startTime,
                                    timeRuleType);
                    break;
            }
            // For now, use ID + "(DST)" as the name
            dstRule =
                    new AnnualTimeZoneRule(
                            getID() + "(DST)",
                            getRawOffset(),
                            getDSTSavings(),
                            dtRule,
                            startYear,
                            AnnualTimeZoneRule.MAX_YEAR);

            // Calculate the first DST start time
            firstDstStart = dstRule.getFirstStart(getRawOffset(), 0).getTime();

            // Create a TimeZoneRule for standard time
            timeRuleType =
                    (endTimeMode == STANDARD_TIME)
                            ? DateTimeRule.STANDARD_TIME
                            : ((endTimeMode == UTC_TIME)
                                    ? DateTimeRule.UTC_TIME
                                    : DateTimeRule.WALL_TIME);
            switch (endMode) {
                case DOM_MODE:
                    dtRule = new DateTimeRule(endMonth, endDay, endTime, timeRuleType);
                    break;
                case DOW_IN_MONTH_MODE:
                    dtRule =
                            new DateTimeRule(endMonth, endDay, endDayOfWeek, endTime, timeRuleType);
                    break;
                case DOW_GE_DOM_MODE:
                    dtRule =
                            new DateTimeRule(
                                    endMonth, endDay, endDayOfWeek, true, endTime, timeRuleType);
                    break;
                case DOW_LE_DOM_MODE:
                    dtRule =
                            new DateTimeRule(
                                    endMonth, endDay, endDayOfWeek, false, endTime, timeRuleType);
                    break;
            }
            // For now, use ID + "(STD)" as the name
            stdRule =
                    new AnnualTimeZoneRule(
                            getID() + "(STD)",
                            getRawOffset(),
                            0,
                            dtRule,
                            startYear,
                            AnnualTimeZoneRule.MAX_YEAR);

            // Calculate the first STD start time
            firstStdStart =
                    stdRule.getFirstStart(getRawOffset(), dstRule.getDSTSavings()).getTime();

            // Create a TimeZoneRule for initial time
            if (firstStdStart < firstDstStart) {
                initialRule =
                        new InitialTimeZoneRule(
                                getID() + "(DST)", getRawOffset(), dstRule.getDSTSavings());
                firstTransition = new TimeZoneTransition(firstStdStart, initialRule, stdRule);
            } else {
                initialRule = new InitialTimeZoneRule(getID() + "(STD)", getRawOffset(), 0);
                firstTransition = new TimeZoneTransition(firstDstStart, initialRule, dstRule);
            }

        } else {
            // Create a TimeZoneRule for initial time
            initialRule = new InitialTimeZoneRule(getID(), getRawOffset(), 0);
        }
        transitionRulesInitialized = true;
    }

    // Freezable stuffs
    private transient volatile boolean isFrozen = false;

    /**
     * {@inheritDoc}
     *
     * @stable ICU 49
     */
    @Override
    public boolean isFrozen() {
        return isFrozen;
    }

    /**
     * {@inheritDoc}
     *
     * @stable ICU 49
     */
    @Override
    public TimeZone freeze() {
        isFrozen = true;
        return this;
    }

    /**
     * {@inheritDoc}
     *
     * @stable ICU 49
     */
    @Override
    public SimpleTimeZone cloneAsThawed() {
        SimpleTimeZone tz = (SimpleTimeZone) super.cloneAsThawed();
        tz.isFrozen = false;
        return tz;
    }
}
